require 'socket'
require 'open-uri'
require 'esi/base_parser'
require 'hpricot'
require 'esi/parser'

module ESI
  class StreamCallBack
    def initialize(callback)
      @callback = callback
    end
    def << (output) 
      @callback.call( output )
    end
  end
  # process a input stream in one pass
  # writing to the output stream as soon as the bytes appear to not be within an esi tag
  # this will maximize percieved response time, and allow the browser to get to downloading css/js faster
  # ideally, we'd do the following:
  # stop streaming on the first esi tag encountered, and continue parsing for esi tags,
  # buffering and marking any new esi tags,
  # however in reality we stop parsing until we finish processing an esi tag.
  class StreamParser < BaseParser

    def process_io(options, &output_callback )
      input = options[:input_stream]
      @request_params = options[:request_params]
      depth = options[:depth] || 0
      @config = options[:config] || {}
      @fragment_params = options[:http_params]
      @fragment_params = @fragment_params.reject{|k,v| k == 'Content-Type' || k == 'Content-Length' || k == 'Surrogate-Control' }
      @stream_index = 0

      parse_buffer( input.read_body, StreamCallBack.new(output_callback) )
    end

    def parse_buffer( input, output, depth = 0 )

      input_end = input.size

      # scan the input searching for an esi tag
      while( (esi_index = input.index("<esi:")) )
        # stream what we can to the client
        output << input[@stream_index..(esi_index-1)] unless @stream_index == esi_index

        # update input stream to exclude streamed data
        input = input[esi_index..input_end]

        # get the tag name
        tag = input.scan( /<esi:(\w+)/ ).first

        # end will either </esi:#{tag}> or <esi:tag .*?/>
        if input.match( /<esi:#{tag}.*\/>/ )
          block_end = input.index("/>") + 1
        else
          # check for block tag
          block_end = input.index("</esi:#{tag}>") + "</esi:#{tag}>".size - 1
        end

        # extract everything within the esi tag context e.g. could be
        # <esi:try><esi:attempt><esi:include.../></esi:attempt>...</try>
        # or <esi:include .../>
        esi_block = input[0..block_end]

        # read the block of esi input
        process_esi_tags( esi_block, output, depth )
        #parser.parse( esi_block )

        # update input to exclude processed esi fragment
        input = input[(block_end+1)..input.size]

      end

      output << input
    end

private

    def process_esi_tags( esi_block, output, depth )
      doc = Hpricot.XML(esi_block) # use hpricot to process the esi block
      process_esi_invalidations( doc, output )
      process_esi_includes( doc, output, depth )
    end

    def process_esi_includes( doc, output, depth )

      # if there's a try look in the attempt block first
      # preserve markup within the attempt, strip the esi tags replace any includes with
      # the response from the fragment server.  If any includes fail, try the except block instead
      el_try = doc.at("esi:try")
      el_includes = []
      el_except_block = nil
      el_parent = nil

      if el_try
        el_parent = el_try.at("esi:attempt")
        el_includes = el_parent.search("esi:include")
      else
        edge_include = doc.at("esi:include") # handle special case of a lone esi include without a try catch block
        if edge_include
          el_includes << edge_include
          el_parent = doc
        end
      end

      if !el_includes.empty?

        exception, url = request_includes( el_includes, el_parent, output, depth )

        if exception

          el_except_block = el_try.at("esi:except")
          el_includes = el_except_block.search("esi:include")
          exception, url = request_includes( el_includes, el_except_block, output, depth )

          if el_includes.size == 0
            output << el_except_block.inner_html
          end

          if exception
            error = "HTTP Error: #{exception.message}, #{url}"
            $logger.error error
          end

        end
      end
    end

    def request_includes( el_includes, parent, output, depth )
      exception = nil
      el_includes.each do|el_include|
        fragment_response, error = fetch_include( el_include )

        if !error
          if depth < 3 # we lock the depth at 3 to match oracle's esi implementation, could make this configurable
            buffer = ""
            parse_buffer( fragment_response, buffer, depth + 1 )
            fragment_response = buffer
          end
        else
          $logger.error "Attempting failover on include => #{el_include.attributes['src']}"
          exception = [error,el_include.attributes['src']]
          break
        end
        el_include.swap( fragment_response )
      end
      output << parent.inner_html unless exception
      exception
    end

    def fetch_include(el_include)
      url = @router.url_for(prepare_url_vars(el_include.attributes["src"],@request_params))
      failover = false
      fragment_response = nil
      error = nil # use this flag to indicate if the request failed or not

      begin
        if @cache.cached?( url, @fragment_params )
          fragment_response = @cache.get(url,@fragment_params).body
          $request_logger.debug "C"
        else
          timeout = el_include.attributes["timeout"]
          #params = (!timeout.blank? and RUBY_VERSION >= '1.9.0') ? @fragment_params.merge(:read_timeout => nil) : @fragment_params
          params = @fragment_params
          if timeout.blank?
            fragment_response = open( url, params ).read
          else
            fragment_response = Timeout::timeout(timeout.to_i) { open( url, params ).read }
          end
          ttl = (el_include.attributes["max-age"] || @router.cache_ttl( url ) )
          # if the include doesn't contain a ttl, first check the route config to see if one was defined
          ttl = (@config[:cache_ttl] || 0) unless ttl # assume this means the user wants caching disabled and don't cache
          @cache.put(url, @fragment_params, ttl, fragment_response)
          $request_logger.debug "R"
        end
      rescue Timeout::Error, Exception, OpenURI::HTTPError => e
        error = e
        unless failover
          alt = el_include.attributes["alt"]
          if alt
            retry_url = @router.url_for(prepare_url_vars(alt,@request_params))
            if( retry_url and retry_url != url )
              failover = true
              url = retry_url
              error = nil
              retry
            end
          end
          fragment_response = "HTTP Error: #{e.message}, <a href='#{url}'>View Fragment</a>"
          fragment_response << %Q(<div class='block' style='z-index:400;overflow:auto;background:gray;font-weight:bold;'><p>Failover failed<br/>#{e.backtrace}</p></div>)
        end
      end
 
      [ fragment_response, error ]
    end

    def process_esi_invalidations( doc, output )
      (doc/"esi:invalidate").each do |invalidate|
        xml = Hpricot.XML(invalidate.inner_html)

        (xml/:OBJECT).each do|object|
          selector = (object % :BASICSELECTOR)
          advanced_selector = (object % :ADVANCEDSELECTOR)
          if selector
            action = (object % :ACTION)
            info = (object % :INFO)
            uri = selector[:URI]
            remove_ttl = action[:REMOVALTTL].to_i
            @cache.keys do |key,fragment|
              if fragment.uri.match(uri)
                @cache.delete_unlocked( key )
              end
            end
          elsif advanced_selector
            # XXX: this code is untested!
            # NOTE: our support of both advanced and basic is a little off
            # we'll probably invalidate more then is normally done by the standard
            # also when we switch to memcache this will be much harder to support this flexibilty
            uri_prefix = advanced_selector[:URIPREFIX]
            uri_regex = advanced_selector[:URIEXP]
						@cache.keys do |key,data|
							if key.match(uri_prefix) and key.match(uri_regex)
                puts "invalidating #{uri}"
                # XXX: this code is untested!
								@cache.delete_unlocked( key, remove_ttl )
							end
						end
            # XXX: this code is untested!
          end
        end
        @cache.sweep!
      end
    end

  end
end
